package com.spectre.generate;

import com.google.auto.service.AutoService;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.spectre.generate.annotation.ConfigGeneration;
import com.spectre.generate.annotation.DefaultValue;
import com.spectre.generate.annotation.IntegerValue;
import com.spectre.generate.exception.ConfigFileNotFoundException;
import com.spectre.generate.exception.IntegerValueLimitException;
import com.spectre.generate.info.ClassMetaInfo;
import com.spectre.generate.info.ConfigMetaInfo;
import com.spectre.generate.info.FieldInfo;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

/**
 * 配置类生成过程类
 */
@AutoService(Processor.class)
public class ConfigProcessor extends AbstractProcessor {
    private Elements mElementUtils;
    private Messager mMessager;
    private Filer mFiler;
    private Types mTypeUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // 元素操作辅助工具
        mElementUtils = processingEnvironment.getElementUtils();
        // 日志辅助工具
        mMessager = processingEnvironment.getMessager();
        // 文件操作辅助工具
        mFiler = processingEnvironment.getFiler();
        mTypeUtils = processingEnvironment.getTypeUtils();
    }

    /**
     * 1. set：携带getSupportedAnnotationTypes()中的注解类型。
     * 2. roundEnvironment：processor将扫描到的信息存储到roundEnvironment中，从这里取出所有使用ConfigGeneration注解的字段。
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        try {
            List<ConfigMetaInfo> configMetaInfos = new ArrayList<>();
            Set<? extends Element> generationElements = roundEnvironment.getElementsAnnotatedWith(ConfigGeneration.class);
            for (Element element : generationElements) {
                //生成对应实体类,将对应配置的描述信息存入集合
                configMetaInfos.add(generateEntityClass(element));
            }
            //生成管理类,把所有配置类注册到管理类中(需要知道类名)
            generateManagerClass(configMetaInfos);

        } catch (Exception e) {
            e.printStackTrace();
        }

        return true;
    }

    /**
     * 生成ConfigManager类,用于管理所有配置实例
     */
    private void generateManagerClass(List<ConfigMetaInfo> configs) {
        try {
            //添加类名
            TypeSpec.Builder builder = TypeSpec.enumBuilder("ConfigManager")
                    .addModifiers(Modifier.PUBLIC);

            builder.addEnumConstant("INSTANCE");

            FieldSpec gsonFiled = FieldSpec.builder(Gson.class, "gson")
                    .addModifiers(Modifier.PRIVATE)
                    .build();
            builder.addField(gsonFiled);

            //添加init方法
            MethodSpec initMethod = MethodSpec.methodBuilder("init")
                    .addModifiers(Modifier.PUBLIC)
                    .addException(ConfigFileNotFoundException.class)
                    .addStatement("gson = new $T().serializeNulls().setPrettyPrinting().create()", GsonBuilder.class)
                    .addStatement("loadConfig()")
                    .build();
            builder.addMethod(initMethod);


            //生成读取json文件方法
            MethodSpec parseConfigMethod = MethodSpec.methodBuilder("parseConfigFile")
                    .addTypeVariable(TypeVariableName.get("T", Checkable.class))
                    .addParameter(File.class, "file")
                    .addParameter(ParameterizedTypeName.get(ClassName.get(Class.class)
                            , TypeVariableName.get("T", Checkable.class)), "clazz")
                    .addException(ConfigFileNotFoundException.class)
                    .returns(TypeVariableName.get("T"))
                    //如果不存在就生成json文件
                    .beginControlFlow("if (!file.exists())")
                    .addStatement("$T fileWriter = null", FileWriter.class)
                    .beginControlFlow("try")
                    .beginControlFlow("if (!file.getParentFile().exists())")
                    .addStatement("file.getParentFile().mkdirs()")
                    .endControlFlow()
                    .addStatement("file.createNewFile()")
                    .addStatement("T t = clazz.newInstance()")
                    .addStatement("$T jsonStr = gson.toJson(t)", String.class)
                    .addStatement("fileWriter = new $T(file)", FileWriter.class)
                    .addStatement("fileWriter.write(jsonStr)")
                    .addStatement("fileWriter.flush()")
                    .nextControlFlow("catch ($T e)", Exception.class)
                    .addStatement("e.printStackTrace()")
                    .nextControlFlow("finally")
                    .beginControlFlow("if (fileWriter != null)")
                    .beginControlFlow("try")
                    .addStatement("fileWriter.close()")
                    .nextControlFlow("catch ($T e)", IOException.class)
                    .addStatement("e.printStackTrace()")
                    .endControlFlow()
                    .endControlFlow()
                    .endControlFlow()
                    .endControlFlow()

                    //读取json文件
                    .addStatement("$T fileReader = null", FileReader.class)
                    .addStatement("T type = null")
                    .beginControlFlow("try")
                    .addStatement("fileReader = new $T(file)", FileReader.class)
                    .addStatement("type = gson.fromJson(fileReader, clazz)")
                    .addStatement("type.check()")
                    .nextControlFlow("catch ($T e)", FileNotFoundException.class)
                    .addStatement("e.printStackTrace()")
                    .addStatement("throw new $T(\"配置文件:\" + file.getAbsolutePath() + \"缺失\")", ConfigFileNotFoundException.class)
                    .nextControlFlow("finally")
                    .beginControlFlow("if (fileReader != null)")
                    .beginControlFlow("try")
                    .addStatement("fileReader.close()")
                    .nextControlFlow("catch ($T e)", IOException.class)
                    .addStatement("e.printStackTrace()")
                    .endControlFlow()
                    .endControlFlow()
                    .endControlFlow()
                    .addStatement("return type")
                    .addModifiers(Modifier.PUBLIC)
                    .build();
            builder.addMethod(parseConfigMethod);

            //配置读取方法
            MethodSpec.Builder loadConfigMethodBuilder = MethodSpec.methodBuilder("loadConfig")
                    .addModifiers(Modifier.PUBLIC)
                    .addException(ConfigFileNotFoundException.class);

            for (ConfigMetaInfo configMetaInfo : configs) {
                //生成字段
                ClassMetaInfo classMetaInfo = configMetaInfo.classMetaInfo;
                ClassName type = ClassName.get(classMetaInfo.packageName, classMetaInfo.name);

                FieldSpec field = FieldSpec.builder(type, "_" + classMetaInfo.name)
                        .addModifiers(Modifier.PRIVATE)
                        .build();
                builder.addField(field);

                // 生成get方法
                StringBuilder sb = new StringBuilder();
                sb.delete(0, sb.length());
                sb.append("get").append(classMetaInfo.name.substring(0, 1).toUpperCase())
                        .append(classMetaInfo.name.substring(1, classMetaInfo.name.length()));
                MethodSpec getMethod = MethodSpec.methodBuilder(sb.toString())
                        .addModifiers(Modifier.PUBLIC)
                        .addStatement("return this._$L", classMetaInfo.name)
                        .returns(type)
                        .build();
                builder.addMethod(getMethod);

                ClassName environmentClassName = ClassName.get("android.os", "Environment");
                String newClassName = configMetaInfo.classMetaInfo.name;

                //添加配置读取方法
                String localFileName = "file" + newClassName;
                loadConfigMethodBuilder
                        .addStatement("$T " + localFileName + " = new $T($T.getExternalStorageDirectory(),$S)"
                                , File.class, File.class, environmentClassName, configMetaInfo.relativePath)
                        .addStatement("_" + newClassName + " = parseConfigFile(" + localFileName + "," + newClassName + ".class)");
            }
            builder.addMethod(loadConfigMethodBuilder.build());

            //生成文件
            JavaFile javaFile = JavaFile.builder("com.spectre.generate", builder.build())
                    .indent("    ")
                    .build();
            javaFile.writeTo(mFiler);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private ConfigMetaInfo generateEntityClass(Element element) {
        // 获取注解目标所在的包
        PackageElement packageElement = mElementUtils.getPackageOf(element);

        //包名
        String pkgName = packageElement.getQualifiedName().toString();

        //原类名
        String name = element.getSimpleName().toString();

        //生成类名
        String className = name + "Config";

        ClassMetaInfo classMetaInfo = new ClassMetaInfo();
        classMetaInfo.packageName = pkgName;
        classMetaInfo.name = className;

        List<FieldInfo> fields = new ArrayList<>();

        for (Element enclosedElement : element.getEnclosedElements()) {
            if (enclosedElement.getKind() == ElementKind.FIELD) {
                //获取字段注解
                IntegerValue integerValueLimit = enclosedElement.getAnnotation(IntegerValue.class);
                DefaultValue defaultValue = enclosedElement.getAnnotation(DefaultValue.class);

                // 获取字段名
                String filedName = enclosedElement.getSimpleName().toString();
                // 获取字段类型
                TypeName fieldType = ClassName.get(enclosedElement.asType());

                FieldInfo fieldInfo = new FieldInfo(filedName, fieldType, integerValueLimit, defaultValue);
                fields.add(fieldInfo);
            }
        }
        classMetaInfo.fields = fields;

        generateJavaFile(classMetaInfo);

        //类描述信息,配置文件相对路径
        ConfigGeneration annotation = element.getAnnotation(ConfigGeneration.class);
        String relativePath = annotation.relativePath();
        ConfigMetaInfo configMetaInfo = new ConfigMetaInfo(classMetaInfo, relativePath);
        return configMetaInfo;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add(ConfigGeneration.class.getCanonicalName());
        return types;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        SourceVersion version = SourceVersion.RELEASE_8;
        return version;
    }

    /**
     * 使用javapoet生成java文件
     */
    private void generateJavaFile(ClassMetaInfo classMetaInfo) {
        try {
            //添加类名
            TypeSpec.Builder builder = TypeSpec.classBuilder(classMetaInfo.name)
                    .addModifiers(Modifier.PUBLIC);

            // 生成构造函数
            MethodSpec constructor = MethodSpec.constructorBuilder()
//                    .addParameter(ParameterSpec.builder(targetClass, "target").build())
                    .addModifiers(Modifier.PUBLIC)
                    .build();

            //存放需要做值检查的字段
            Map<String, IntegerValue> map = new HashMap<>();

            //生成所有字段
            for (FieldInfo fieldInfo : classMetaInfo.fields) {

                //生成字段
                FieldSpec.Builder targetFieldBuilder = FieldSpec.builder(fieldInfo.type, fieldInfo.name)
                        .addModifiers(Modifier.PRIVATE);

                //字段类型添加默认值
                if (fieldInfo.defaultValue != null) {
                    DefaultValue defaultValue = fieldInfo.defaultValue;
                    if (TypeName.BOOLEAN.equals(fieldInfo.type)) {
                        targetFieldBuilder.initializer(String.valueOf(defaultValue.defaultBooleanValue()));
                    } else if (TypeName.INT.equals(fieldInfo.type)||TypeName.LONG.equals(fieldInfo.type)) {
                        targetFieldBuilder.initializer(String.valueOf(defaultValue.defaultIntegerValue()));
                    } else if (TypeName.FLOAT.equals(fieldInfo.type)) {
                        targetFieldBuilder.initializer(defaultValue.defaultFloatValue()+"f");
                    } else if ("java.lang.String".equals(fieldInfo.type.toString())) {
                        targetFieldBuilder.initializer("$S", defaultValue.defaultStringValue());
                    } else if ("java.util.List<java.lang.Integer>".equals(fieldInfo.type.toString())) {
                        int[] ints = defaultValue.defaultIntArrayValue();
                        if (ints != null && ints.length > 0) {
                            StringBuilder stringBuilder = new StringBuilder();
                            stringBuilder.append("$T.asList(");
                            for (int i = 0; i < ints.length; i++) {
                                stringBuilder.append(ints[i]);
                                if (i != ints.length - 1) {
                                    stringBuilder.append(",");
                                }
                            }
                            stringBuilder.append(")");

                            targetFieldBuilder.initializer(stringBuilder.toString(), Arrays.class);
                        }
                    } else if ("java.util.List<java.lang.String>".equals(fieldInfo.type.toString())) {
                        String[] strs = defaultValue.defaultStringArrayValue();
                        if (strs != null && strs.length > 0) {
                            StringBuilder stringBuilder = new StringBuilder();
                            stringBuilder.append("$T.asList(");
                            for (int i = 0; i < strs.length; i++) {
                                stringBuilder.append("\"" + strs[i] + "\"");
                                if (i != strs.length - 1) {
                                    stringBuilder.append(",");
                                }
                            }
                            stringBuilder.append(")");

                            targetFieldBuilder.initializer(stringBuilder.toString(), Arrays.class);
                        }
                    }
                }

                FieldSpec targetField = targetFieldBuilder.build();

                builder.addField(targetField);

                // 生成set方法
                StringBuilder sb = new StringBuilder();
                sb.append("set").append(fieldInfo.name.substring(0, 1).toUpperCase())
                        .append(fieldInfo.name.substring(1, fieldInfo.name.length()));

                MethodSpec.Builder setMethodBuilder = MethodSpec.methodBuilder(sb.toString())
                        .addParameter(fieldInfo.type, fieldInfo.name)
                        .addJavadoc(fieldInfo.type.toString())
                        .addModifiers(Modifier.PUBLIC);

                //被限制注解标记的需要添加值检查方法
                if (fieldInfo.integerValueLimit != null) {
                    map.put(fieldInfo.name, fieldInfo.integerValueLimit);
                }

                setMethodBuilder
                        .addStatement("this.$L = $L", fieldInfo.name, fieldInfo.name);

                builder.addMethod(setMethodBuilder.build());

                // 生成get方法
                sb.delete(0, sb.length());
                sb.append("get").append(fieldInfo.name.substring(0, 1).toUpperCase())
                        .append(fieldInfo.name.substring(1, fieldInfo.name.length()));
                MethodSpec getMethod = MethodSpec.methodBuilder(sb.toString())
                        .addModifiers(Modifier.PUBLIC)
                        .addStatement("return this.$L", fieldInfo.name)
                        .returns(fieldInfo.type)
                        .build();
                builder.addMethod(getMethod);
            }

            //生成值检查方法
            MethodSpec.Builder checkMethodBuilder = MethodSpec.methodBuilder("check")
                    .addModifiers(Modifier.PUBLIC);

            if (!map.isEmpty()) {
                Set<String> keys = map.keySet();
                for (String name : keys) {
                    IntegerValue integerValueLimit = map.get(name);
                    int min = integerValueLimit.min();
                    int max = integerValueLimit.max();

                    checkMethodBuilder
                            .beginControlFlow("if(" + name + "<" + min + "||" + name + ">" + max + ")")
                            .addStatement("throw new $T($S)", IntegerValueLimitException.class, name + "参数超出允许的范围!")
                            .endControlFlow();
                }
            }

            builder.addMethod(checkMethodBuilder.build());

            builder.addMethod(constructor);

            builder.addSuperinterface(Checkable.class);

            //生成文件
            JavaFile javaFile = JavaFile.builder(classMetaInfo.packageName, builder.build())
                    .indent("    ")
                    .build();
            javaFile.writeTo(mFiler);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

